
using System;
using System.Runtime.InteropServices;
using Assimp.Unmanaged;

namespace Assimp {
    /// <summary>
    /// Defines a stream to some file input or output source. This object is responsible for reading/writing data
    /// that is used by Assimp.
    /// </summary>
    public abstract class IOStream : IDisposable {
        // Don't delete these, holding onto the callbacks prevent them from being GC'ed inappropiately
        private AiFileWriteProc m_writeProc;
        private AiFileReadProc m_readProc;
        private AiFileTellProc m_tellProc;
        private AiFileTellProc m_fileSizeProc;
        private AiFileSeek m_seekProc;
        private AiFileFlushProc m_flushProc;
        private IntPtr m_filePtr;
        private bool m_isDiposed;
        private String m_pathToFile;
        private FileIOMode m_fileMode;
        private byte[] m_byteBuffer;

        /// <summary>
        /// Gets whether or not this IOStream has been disposed.
        /// </summary>
        public bool IsDisposed {
            get {
                return m_isDiposed;
            }
        }

        /// <summary>
        /// Gets the original path to file given by Assimp.
        /// </summary>
        public String PathToFile {
            get {
                return m_pathToFile;
            }
        }

        /// <summary>
        /// Gets the original desired file access mode.
        /// </summary>
        public FileIOMode FileMode {
            get {
                return m_fileMode;
            }
        }

        /// <summary>
        /// Gets whether the stream is in fact valid - that is, the input/output has been
        /// properly located and can be read/written.
        /// </summary>
        public abstract bool IsValid {
            get;
        }

        internal IntPtr AiFile {
            get {
                return m_filePtr;
            }
        }

        /// <summary>
        /// Constructs a new IOStream.
        /// </summary>
        /// <param name="pathToFile">Path to file given by Assimp</param>
        /// <param name="fileMode">Desired file access mode</param>
        public IOStream(String pathToFile, FileIOMode fileMode) : this(pathToFile, fileMode, true) { }

        /// <summary>
        /// Constructs a new IOStream.
        /// </summary>
        /// <param name="pathToFile">Path to file given by Assimp</param>
        /// <param name="fileMode">Desired file access mode</param>
        /// <param name="initialize">True if initialize should be immediately called with the default callbacks. Set this to false
        /// if your subclass requires a different way to setup the function pointers.</param>
        protected IOStream(String pathToFile, FileIOMode fileMode, bool initialize = true) {
            m_pathToFile = pathToFile;
            m_fileMode = fileMode;

            if (initialize)
                Initialize(OnAiFileWriteProc, OnAiFileReadProc, OnAiFileTellProc, OnAiFileSizeProc, OnAiFileSeekProc, OnAiFileFlushProc);
        }

        /// <summary>
        /// Initializes the system by setting up native pointers for Assimp to the specified functions. A reference to each
        /// supplied callback is held on to avoid it being GC'ed.
        /// </summary>
        /// <param name="aiFileWriteProc">Handles write requests.</param>
        /// <param name="aiFileReadProc">Handles read requests.</param>
        /// <param name="aiFileTellProc">Handles tell requests.</param>
        /// <param name="aiFileSizeProc">Handles size requests.</param>
        /// <param name="aiFileSeek">Handles seek requests.</param>
        /// <param name="aiFileFlushProc">Handles flush requests.</param>
        /// <param name="userData">Additional user data, if any.</param>
        protected void Initialize(AiFileWriteProc aiFileWriteProc, AiFileReadProc aiFileReadProc, AiFileTellProc aiFileTellProc, AiFileTellProc aiFileSizeProc, AiFileSeek aiFileSeek, AiFileFlushProc aiFileFlushProc, IntPtr userData = default) {
            m_writeProc = aiFileWriteProc;
            m_readProc = aiFileReadProc;
            m_tellProc = aiFileTellProc;
            m_fileSizeProc = aiFileSizeProc;
            m_seekProc = aiFileSeek;
            m_flushProc = aiFileFlushProc;

            AiFile file;
            file.WriteProc = Marshal.GetFunctionPointerForDelegate(aiFileWriteProc);
            file.ReadProc = Marshal.GetFunctionPointerForDelegate(aiFileReadProc);
            file.TellProc = Marshal.GetFunctionPointerForDelegate(aiFileTellProc);
            file.FileSizeProc = Marshal.GetFunctionPointerForDelegate(aiFileSizeProc);
            file.SeekProc = Marshal.GetFunctionPointerForDelegate(aiFileSeek);
            file.FlushProc = Marshal.GetFunctionPointerForDelegate(aiFileFlushProc);
            file.UserData = userData;

            m_filePtr = MemoryHelper.AllocateMemory(MemoryHelper.SizeOf<AiFile>());
            Marshal.StructureToPtr(file, m_filePtr, false);
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="IOStream"/> class.
        /// </summary>
        ~IOStream() {
            Dispose(false);
        }

        /// <summary>
        /// Disposes of resources held by the IOStream.
        /// </summary>
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing">True to release both managed and unmanaged resources; False to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing) {
            if (!m_isDiposed) {
                if (m_filePtr != IntPtr.Zero) {
                    MemoryHelper.FreeMemory(m_filePtr);
                    m_filePtr = IntPtr.Zero;
                }

                if (disposing) {
                    m_writeProc = null;
                    m_readProc = null;
                    m_tellProc = null;
                    m_fileSizeProc = null;
                    m_seekProc = null;
                    m_flushProc = null;
                }

                m_isDiposed = true;
            }
        }

        /// <summary>
        /// Writes data to the stream.
        /// </summary>
        /// <param name="dataToWrite">Data to write</param>
        /// <param name="count">Number of bytes to write</param>
        /// <returns>Number of bytes actually written. Should be equal to the specified count, unless if EoF was hit or an error occured.</returns>
        public abstract long Write(byte[] dataToWrite, long count);

        /// <summary>
        /// Reads data from the stream.
        /// </summary>
        /// <param name="dataRead">Byte buffer to store the read data in</param>
        /// <param name="count">Number of bytes to read</param>
        /// <returns>Number of bytes actually read. Should be equal to the specified count, unless if EoF was hit or an error occured.</returns>
        public abstract long Read(byte[] dataRead, long count);

        /// <summary>
        /// Sets the current file position pointer.
        /// </summary>
        /// <param name="offset">Offset in bytes from the origin</param>
        /// <param name="seekOrigin">Origin reference</param>
        /// <returns>ReturnCode indicating success or failure.</returns>
        public abstract ReturnCode Seek(long offset, Origin seekOrigin);

        /// <summary>
        /// Gets the current file position pointer (in bytes).
        /// </summary>
        /// <returns>Current file position pointer (in bytes)</returns>
        public abstract long GetPosition();

        /// <summary>
        /// Gets the total file size (in bytes).
        /// </summary>
        /// <returns>File size in bytes</returns>
        public abstract long GetFileSize();

        /// <summary>
        /// Flushes all data currently in the stream buffers.
        /// </summary>
        public abstract void Flush();

        /// <summary>
        /// Closes the stream - flushing any data not yet read/written and disposes of resources.
        /// </summary>
        public virtual void Close() {
            Flush();
            Dispose();
        }

        /// <summary>
        /// Callback for Assimp that handles writes.
        /// </summary>
        /// <param name="file"></param>
        /// <param name="dataToWrite"></param>
        /// <param name="sizeOfElemInBytes"></param>
        /// <param name="numElements"></param>
        /// <returns></returns>
        protected UIntPtr OnAiFileWriteProc(IntPtr file, IntPtr dataToWrite, UIntPtr sizeOfElemInBytes, UIntPtr numElements) {
            if (m_filePtr != file)
                return UIntPtr.Zero;

            long longSize = (long)sizeOfElemInBytes.ToUInt64();
            long longNum = (long)numElements.ToUInt64();
            long count = longSize * longNum;

            if (count == 0)
                return UIntPtr.Zero;

            byte[] byteBuffer = GetByteBuffer(longSize, longNum);
            MemoryHelper.Read<byte>(dataToWrite, byteBuffer, 0, (int)count);

            long actualCount = 0;

            try {
                actualCount = Write(byteBuffer, count);
            }
            catch (Exception) { /*Assimp will report an IO error*/ }

            return new UIntPtr((ulong)actualCount / (ulong)longSize);
        }

        /// <summary>
        /// Callback for Assimp that handles reads.
        /// </summary>
        /// <param name="file"></param>
        /// <param name="dataRead"></param>
        /// <param name="sizeOfElemInBytes"></param>
        /// <param name="numElements"></param>
        /// <returns></returns>
        protected UIntPtr OnAiFileReadProc(IntPtr file, IntPtr dataRead, UIntPtr sizeOfElemInBytes, UIntPtr numElements) {
            if (m_filePtr != file)
                return UIntPtr.Zero;

            long longSize = (long)sizeOfElemInBytes.ToUInt64();
            long longNum = (long)numElements.ToUInt64();
            long count = longSize * longNum;

            byte[] byteBuffer = GetByteBuffer(longSize, longNum);

            long actualCount = 0;

            try {
                actualCount = Read(byteBuffer, count);

                if (actualCount > 0)
                    MemoryHelper.Write<byte>(dataRead, byteBuffer, 0, (int)actualCount);
            }
            catch (Exception) { /*Assimp will report an IO error*/ }

            return new UIntPtr((ulong)actualCount / (ulong)longSize);
        }

        /// <summary>
        /// Callback for Assimp that handles tell requests.
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        protected UIntPtr OnAiFileTellProc(IntPtr file) {
            if (m_filePtr != file)
                return UIntPtr.Zero;

            long pos = 0;

            try {
                pos = GetPosition();
            }
            catch (Exception) { /*Assimp will report an IO error*/ }

            return new UIntPtr((ulong)pos);
        }

        /// <summary>
        /// Callback for Assimp that handles size requests.
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        protected UIntPtr OnAiFileSizeProc(IntPtr file) {
            if (m_filePtr != file)
                return UIntPtr.Zero;

            long fileSize = 0;

            try {
                fileSize = GetFileSize();
            }
            catch (Exception) { /*Assimp will report an IO error*/ }

            return new UIntPtr((ulong)fileSize);
        }

        /// <summary>
        /// Callback for Assimp that handles seeks.
        /// </summary>
        /// <param name="file"></param>
        /// <param name="offset"></param>
        /// <param name="seekOrigin"></param>
        /// <returns></returns>
        protected ReturnCode OnAiFileSeekProc(IntPtr file, UIntPtr offset, Origin seekOrigin) {
            if (m_filePtr != file)
                return ReturnCode.Failure;

            ReturnCode code = ReturnCode.Failure;

            try {
                code = Seek((long)offset.ToUInt64(), seekOrigin);
            }
            catch (Exception) { /*Assimp will report an IO error*/ }

            return code;
        }

        /// <summary>Callback for Assimp that handles flushes.</summary>
        /// <param name="file"></param>
        protected void OnAiFileFlushProc(IntPtr file) {
            if (m_filePtr != file)
                return;

            try {
                Flush();
            }
            catch (Exception) { }
        }

        private byte[] GetByteBuffer(long sizeOfElemInBytes, long numElements) {
            //Only create a new buffer if we need it to grow or first time, otherwise re-use it
            if (m_byteBuffer == null || (m_byteBuffer.Length < sizeOfElemInBytes * numElements))
                m_byteBuffer = new byte[sizeOfElemInBytes * numElements];

            return m_byteBuffer;
        }
    }
}
